# -*- coding: utf-8 -*-
"""
Created on Sun Oct 12 22:40:05 2014

@author: Themos Tsikas, Jack Richmond
"""

import sys
import time


class RequestError(Exception):
    '''
    An exception that happens when talking to the plate solver
    '''
    pass


def json2python(json):
    '''
    translates JSON to python
    '''
    import ujson
    try:
        return ujson.loads(json)
    except:
        pass
    return None


def python2json(pyd):
    '''
    translates python  to JSON
    '''
    import ujson
    return ujson.dumps(pyd)


class Client(object):
    '''
    nova.astrometry.net client
    '''
    default_url = 'http://nova.astrometry.net/api/'

    def __init__(self, apiurl=default_url):
        self.session = None
        self.apiurl = apiurl

    def get_url(self, service):
        '''
        constructs URL for a plate-solver service
        '''
        return self.apiurl + service

    def send_request(self, service, args={}, file_args=None):
        '''
        service: string
        args: dict
        '''
        from email.mime.base import MIMEBase
        from email.mime.multipart import MIMEMultipart
        from email.encoders import encode_noop
        from urllib2 import urlopen
        from urllib2 import Request
        from urllib2 import HTTPError
        from urllib import urlencode
        from email.mime.application import MIMEApplication
        if self.session is not None:
            args.update({'session': self.session})
        # print 'Python:', (args)
        json = python2json(args)
        # print 'Sending json:', json
        url = self.get_url(service)
        print 'Sending to URL:', url
        # If we're sending a file, format a multipart/form-data
        if file_args is not None:
            ma1 = MIMEBase('text', 'plain')
            ma1.add_header('Content-disposition',
                           'form-data; name="request-json"')
            ma1.set_payload(json)
            ma2 = MIMEApplication(file_args[1], 'octet-stream', encode_noop)
            ma2.add_header('Content-disposition',
                           'form-data; name="file"; filename="%s"'
                           % file_args[0])
            # msg.add_header('Content-Disposition', 'attachment',
            # filename='bud.gif')
            # msg.add_header('Content-Disposition', 'attachment',
            # filename=('iso-8859-1', '', 'FuSballer.ppt'))
            mpa = MIMEMultipart('form-data', None, [ma1, ma2])
            # Makie a custom generator to format it the way we need.
            from cStringIO import StringIO
            from email.generator import Generator

            class MyGenerator(Generator):
                '''
                not sure why we need this, copied from nova's example code
                '''
                def __init__(self, fp, root=True):
                    Generator.__init__(self, fp, mangle_from_=False,
                                       maxheaderlen=0)
                    self.root = root

                def _write_headers(self, msg):
                    # We don't want to write the top-level headers;
                    # they go into Request(headers) instead.
                    if self.root:
                        return
                    # We need to use \r\n line-terminator, but Generator
                    # doesn't provide the flexibility to override, so we
                    # have to copy-n-paste-n-modify.
                    for hoo, voo in msg.items():
                        print >> self._fp, ('%s: %s\r\n' % (hoo, voo)),
                        # A blank line always separates headers from body
                    print >> self._fp, '\r\n',
                    # The _write_multipart method calls "clone" for the
                    # subparts.  We hijack that, setting root=False

                def clone(self, fp):
                    return MyGenerator(fp, root=False)
            fpo = StringIO()
            gen = MyGenerator(fpo)
            gen.flatten(mpa)
            data = fpo.getvalue()
            headers = {'Content-type': mpa.get('Content-type')}
        else:
            # Else send x-www-form-encoded
            data = {'request-json': json}
            # print 'Sending form data:', data
            data = urlencode(data)
            # print 'Sending data:', data
            headers = {}
        request = Request(url=url, headers=headers, data=data)
        try:
            fle = urlopen(request)
            txt = fle.read()
            # print 'Got json:', txt
            result = json2python(txt)
            # print 'Got result:', result
            stat = result.get('status')
            # print 'Got status:', stat
            if stat == 'error':
                errstr = result.get('errormessage', '(none)')
                raise RequestError('server error message: ' + errstr)
            return result
        except HTTPError, err:
            print 'HTTPError', err
            txt = err.read()
            open('err.html', 'wb').write(txt)
            print 'Wrote error text to err.html'

    def login(self, apikey):
        '''
        Logs us into the plate-solver and gets a session key
        '''
        args = {'apikey': apikey}
        result = self.send_request('login', args)
        sess = result.get('session')
        print 'Got session:', sess
        if not sess:
            raise RequestError('no session in result')
        self.session = sess

    def _get_upload_args(self, **kwargs):
        '''
        returns the specified solving options
        '''
        args = {}
        lkdt = [('allow_commercial_use', 'd', str),
                ('allow_modifications', 'd', str),
                ('publicly_visible', 'y', str),
                ('scale_units', None, str),
                ('scale_type', None, str),
                ('scale_lower', None, float),
                ('scale_upper', None, float),
                ('scale_est', None, float),
                ('scale_err', None, float),
                ('center_ra', None, float),
                ('center_dec', None, float),
                ('radius', None, float),
                ('downsample_factor', None, int),
                ('tweak_order', None, int),
                ('crpix_center', None, bool), ]
        for key, default, typ in lkdt:
            # image_width, image_height
            if key in kwargs:
                val = kwargs.pop(key)
                val = typ(val)
                args.update({key: val})
            elif default is not None:
                args.update({key: default})
        # print 'Upload args:', args
        return args

    def upload(self, fne, **kwargs):
        '''
        uploads an image file
        '''
        args = self._get_upload_args(**kwargs)
        try:
            fle = open(fne, 'rb')
            result = self.send_request('upload', args, (fne, fle.read()))
            return result
        except IOError:
            print 'File %s does not exist' % fne
            raise

    def myjobs(self):
        '''
        queries server for our jobs
        '''
        result = self.send_request('myjobs/')
        return result['jobs']

    def job_status(self, job_id, justdict=False):
        '''
        queries server to see if a job is finished
        '''
        result = self.send_request('jobs/%s' % job_id)
        if justdict:
            return result
        stat = result.get('status')
        if stat == 'success':
            return stat
        return stat

    def sub_status(self, sub_id, justdict=False):
        '''
        queries server for submission status
        '''
        result = self.send_request('submissions/%s' % sub_id)
        if justdict:
            return result
        return result.get('status')

    def jobs_by_tag(self, tag, exact):
        '''
        not sure what that does
        '''
        from urllib import quote
        exact_option = 'exact=yes' if exact else ''
        result = self.send_request('jobs_by_tag?query=%s&%s'
                                   % (quote(tag.strip()), exact_option), {}, )
        return result


def stat_bar(self, txt):
    '''
    Update the Status bar
    '''
    self.stat_msg = txt
    self.w_stat.config(text=self.stat_msg)
    self.w_stat.update()


def img2wcs(self, ankey, filename, wcsfn, hint):
    '''
    Plate solves one image
    '''
    import optparse
    from urllib2 import urlopen
    parser = optparse.OptionParser()
    parser.add_option('--server', dest='server', default=Client.default_url,
                      help='Set server base URL (eg, %default)')
    parser.add_option('--apikey', '-k', dest='apikey',
                      help='API key for Astrometry.net web service; if not' +
                      'given will check AN_API_KEY environment variable')
    parser.add_option('--upload', '-u', dest='upload', help='Upload a file')
    parser.add_option('--wait', '-w', dest='wait', action='store_true',
                      help='After submitting, monitor job status')
    parser.add_option('--wcs', dest='wcs',
                      help='Download resulting wcs.fits file, saving to ' +
                      'given filename; implies --wait if --urlupload or' +
                      '--upload')
    parser.add_option('--kmz', dest='kmz',
                      help='Download resulting kmz file, saving to given ' +
                      'filename; implies --wait if --urlupload or --upload')
    parser.add_option('--urlupload', '-U', dest='upload_url',
                      help='Upload a file at specified url')
    parser.add_option('--scale-units', dest='scale_units',
                      choices=('arcsecperpix', 'arcminwidth', 'degwidth',
                               'focalmm'),
                      help='Units for scale estimate')
    parser.add_option('--scale-lower', dest='scale_lower', type=float,
                      help='Scale lower-bound')
    parser.add_option('--scale-upper', dest='scale_upper', type=float,
                      help='Scale upper-bound')
    parser.add_option('--scale-est', dest='scale_est', type=float,
                      help='Scale estimate')
    parser.add_option('--scale-err', dest='scale_err', type=float,
                      help='Scale estimate error (in PERCENT), eg "10" if' +
                      'you estimate can be off by 10%')
    parser.add_option('--ra', dest='center_ra', type=float, help='RA center')
    parser.add_option('--dec', dest='center_dec', type=float,
                      help='Dec center')
    parser.add_option('--radius', dest='radius', type=float,
                      help='Search radius around RA,Dec center')
    parser.add_option('--downsample', dest='downsample_factor', type=int,
                      help='Downsample image by this factor')
    parser.add_option('--parity', dest='parity', choices=('0', '1'),
                      help='Parity (flip) of image')
    parser.add_option('--tweak-order', dest='tweak_order', type=int,
                      help='SIP distortion order (default: 2)')
    parser.add_option('--crpix-center', dest='crpix_center',
                      action='store_true', default=None,
                      help='Set reference point to center of image?')
    parser.add_option('--sdss', dest='sdss_wcs', nargs=2,
                      help='Plot SDSS image for the given WCS file; write ' +
                      'plot to given PNG filename')
    parser.add_option('--galex', dest='galex_wcs', nargs=2,
                      help='Plot GALEX image for the given WCS file; write' +
                      'plot to given PNG filename')
    parser.add_option('--substatus', '-s', dest='sub_id',
                      help='Get status of a submission')
    parser.add_option('--jobstatus', '-j', dest='job_id',
                      help='Get status of a job')
    parser.add_option('--jobs', '-J', dest='myjobs', action='store_true',
                      help='Get all my jobs')
    parser.add_option('--jobsbyexacttag', '-T', dest='jobs_by_exact_tag',
                      help='Get a list of jobs associated with a given' +
                      'tag--exact match')
    parser.add_option('--jobsbytag', '-t', dest='jobs_by_tag',
                      help='Get a list of jobs associated with a given tag')
    parser.add_option('--private', '-p', dest='public', action='store_const',
                      const='n', default='y',
                      help='Hide this submission from other users')
    parser.add_option('--allow_mod_sa', '-m', dest='allow_mod',
                      action='store_const', const='sa', default='d',
                      help='Select license to allow derivative works of ' +
                      'submission, but only if shared under same conditions ' +
                      'of original license')
    parser.add_option('--no_mod', '-M', dest='allow_mod', action='store_const',
                      const='n', default='d',
                      help='Select license to disallow derivative works of ' +
                      'submission')
    parser.add_option('--no_commercial', '-c', dest='allow_commercial',
                      action='store_const', const='n', default='d',
                      help='Select license to disallow commercial use of' +
                      ' submission')
    # load opt with defaults, as above
    opt, args = parser.parse_args([''.split()])
    # add given arguments
    opt.wcs = wcsfn
    opt.apikey = ankey
    opt.upload = filename
    if self.havescale:
        opt.scale_units = 'arcsecperpix'
        opt.scale_est = ("%.2f" % self.scale)
        opt.scale_err = 20
    print opt
    args = {}
    args['apiurl'] = opt.server
    clnt = Client(**args)
    try:
        clnt.login(opt.apikey)
    except RequestError:
        stat_bar(self, ("Couldn't log on to nova.astrometry.net" +
                        "- Check the API key"))
        return
    if opt.upload or opt.upload_url:
        if opt.wcs or opt.kmz:
            opt.wait = True
        kwargs = dict()
        if opt.scale_lower and opt.scale_upper:
            kwargs.update(scale_lower=opt.scale_lower,
                          scale_upper=opt.scale_upper,
                          scale_type='ul')
        elif opt.scale_est and opt.scale_err:
            kwargs.update(scale_est=opt.scale_est,
                          scale_err=opt.scale_err,
                          scale_type='ev')
        elif opt.scale_lower or opt.scale_upper:
            kwargs.update(scale_type='ul')
            if opt.scale_lower:
                kwargs.update(scale_lower=opt.scale_lower)
            if opt.scale_upper:
                kwargs.update(scale_upper=opt.scale_upper)

        for key in ['scale_units', 'center_ra', 'center_dec', 'radius',
                    'downsample_factor', 'tweak_order', 'crpix_center', ]:
            if getattr(opt, key) is not None:
                kwargs[key] = getattr(opt, key)
        if opt.parity is not None:
            kwargs.update(parity=int(opt.parity))
        if opt.upload:
            upres = clnt.upload(opt.upload, **kwargs)
        stat = upres['status']
        if stat != 'success':
            print 'Upload failed: status', stat
            print upres
            sys.exit(-1)
        self.update_upload_labels(hint, "active")
        opt.sub_id = upres['subid']
    if opt.wait:
        if opt.job_id is None:
            if opt.sub_id is None:
                print "Can't --wait without a submission id or job id!"
                sys.exit(-1)
            while True:
                stat = clnt.sub_status(opt.sub_id, justdict=True)
                # print 'Got status:', stat
                jobs = stat.get('jobs', [])
                if len(jobs):
                    for j in jobs:
                        if j is not None:
                            break
                    if j is not None:
                        print 'Selecting job id', j
                        opt.job_id = j
                        break
                time.sleep(5)
        success = False
        while True:
            stat = clnt.job_status(opt.job_id, justdict=True)
            # print 'Got job status:', stat
            if stat.get('status', '') in ['success']:
                success = (stat['status'] == 'success')
                break
            time.sleep(5)
        if success:
            clnt.job_status(opt.job_id)
            retrieveurls = []
            if opt.wcs:
                # We don't need the API for this, just construct URL
                url = opt.server.replace('/api/', '/wcs_file/%i' % opt.job_id)
                retrieveurls.append((url, opt.wcs))
            for url, fne in retrieveurls:
                print 'Retrieving file from', url
                fle = urlopen(url)
                txt = fle.read()
                wfl = open(fne, 'wb')
                wfl.write(txt)
                wfl.close()
                print 'Wrote to', fne
                self.update_solve_labels(hint, "active")
        opt.job_id = None
        opt.sub_id = None
    if opt.sub_id:
        print clnt.sub_status(opt.sub_id)
    if opt.job_id:
        print clnt.job_status(opt.job_id)
    if opt.jobs_by_tag:
        tag = opt.jobs_by_tag
        print clnt.jobs_by_tag(tag, None)
    if opt.jobs_by_exact_tag:
        tag = opt.jobs_by_exact_tag
        print clnt.jobs_by_tag(tag, 'yes')
    if opt.myjobs:
        jobs = clnt.myjobs()
        print jobs

from Tkinter import Frame, Tk, Menu, Label, StringVar, Entry, PhotoImage
from Tkinter import Button, LabelFrame


def help_f():
    '''
    Our help window
    '''
    import tkMessageBox
    tkMessageBox.showinfo("Help", "βοηθηστε αλληλους")


def about_f():
    '''
    our about window
    '''
    import tkMessageBox
    tkMessageBox.showinfo("About",
                          "PhotoPolarAlign v1.0 \n" +
                          "Copyright © 2014 Themos Tsikas, " +
                          "Jack Richmond")


def scale(head):
    '''
    look in the plate-solution header for the scale information
    '''
    comments = head['COMMENT']
    size = (len(comments))
    for i in range(0, size):
        if comments[i][0:5] == 'scale':
            tkns = comments[i].split(" ")
            return float(tkns[1])


def cross(crd, img, colour):
    '''
    Annotate with a cross for the RA axis
    '''
    from PIL import ImageDraw
    draw = ImageDraw.Draw(img)
    coords = crd[0]
    ax1 = coords[0]
    ay1 = coords[1]
    draw.line((ax1 - 30, ay1 - 30) + (ax1 + 30, ay1 + 30),
              fill=colour, width=3)
    draw.line((ax1 + 30, ay1 - 30) + (ax1 - 30, ay1 + 30),
              fill=colour, width=3)


def circle(centre, img, colour, name):
    '''
    Annotate with a circle
    '''
    from PIL import ImageFont, ImageDraw
    font = ImageFont.load("symb24.pil")
    draw = ImageDraw.Draw(img)
    cen = centre[0]
    ax1 = cen[0]
    ay1 = cen[1]
    draw.ellipse((ax1 - 20, ay1 - 20, ax1 + 20, ay1 + 20),
                 fill=None, outline=colour)
    draw.text((ax1 + 30, ay1), name, fill=colour, font=font)


def ncpcircle(centre, img, scl):
    '''
    Annotate with target circles
    '''
    from PIL import ImageFont, ImageDraw
    font = ImageFont.load("helvR24.pil")
    draw = ImageDraw.Draw(img)
    cen = centre[0]
    ax1 = cen[0]
    ay1 = cen[1]
    number = [5, 10, 20, 40]
    for i in number:
        rad = (i*60)/scl
        draw.ellipse((ax1 - rad, ay1 - rad, ax1 + rad, ay1 + rad),
                     fill=None, outline='Green')
        draw.text((ax1 + (rad*26)/36, ay1 + (rad*26/36)), str(i),
                  font=font)


class PhotoPolarAlign(Frame):
    '''
    Our application as a class
    '''
    def write_config_file(self):
        '''
        Update the user preferences file
        '''
        # the API key
        if not self.config.has_section('nova'):
            self.config.add_section('nova')
        self.config.set('nova', 'apikey', self.apikey.get())
        # the image directory
        if not self.config.has_section('file'):
            self.config.add_section('file')
        self.config.set('file', 'imgdir', self.imgdir)
        # the geometry
        if not self.config.has_section('appearance'):
            self.config.add_section('appearance')
        self.config.set('appearance', 'geometry',
                        self.myparent.winfo_geometry())
        #
        with open(self.cfgfn, 'w') as cfgfile:
            self.config.write(cfgfile)
        cfgfile.close()

    def settings_destroy(self):
        '''
        User asked to close the Settings
        '''
        stat_bar(self, "Idle")
        self.write_config_file()
        self.w_var4.configure(text=self.apikey.get())
        self.settings_win.destroy()

    def settings_open(self):
        '''
        Our Settings window
        '''
        from Tkinter import Toplevel
        # create child window
        win = Toplevel()
        self.settings_win = win
        win.geometry('350x150')
        win.title("Settings")
        # get the API key information
        frm = LabelFrame(win, borderwidth=2, relief='ridge', text="Settings")
        frm.pack(side='top', ipadx=20, padx=20, fill='x')
        nxt = Label(frm, text="nova.astrometry.net API Key")
        nxt.grid(row=0, column=0, pady=20, sticky='w')
        nxt = Entry(frm, textvariable=self.apikey)
        nxt.grid(row=0, column=1, pady=20)
        Button(win, text='OK', command=self.settings_destroy).pack(pady=20)

    def quit_method(self):
        '''
        User wants to quit
        '''
        self.write_config_file()
        self.myparent.destroy()

    def get_file(self, hint):
        '''
        User wants to select a file
        '''
        import tkFileDialog
        from os.path import splitext, dirname, basename
        options = {}
        options['filetypes'] = [('JPEG files', '.jpg'), ('all files', '.*')]
        options['initialdir'] = self.imgdir
        if hint == "v":
            options['title'] = 'The (nearly) vertical image of the NCP region'
            self.vimg_fn = tkFileDialog.askopenfilename(**options)
            self.vwcs_fn = splitext(self.vimg_fn)[0] + '.wcs'
            self.imgdir = dirname(self.vimg_fn)
            self.havev = True
            self.w_var1.configure(text=basename(self.vimg_fn))
        elif hint == "h":
            options['title'] = 'The horizontal image of the NCP region'
            self.himg_fn = tkFileDialog.askopenfilename(**options)
            self.hwcs_fn = splitext(self.himg_fn)[0] + '.wcs'
            self.imgdir = dirname(self.himg_fn)
            self.haveh = True
            self.w_var2.configure(text=basename(self.himg_fn))
        elif hint == "i":
            options['title'] = 'The horizontal image after Alt/Az adjustment'
            self.iimg_fn = tkFileDialog.askopenfilename(**options)
            self.iwcs_fn = splitext(self.iimg_fn)[0] + '.wcs'
            self.imgdir = dirname(self.iimg_fn)
            self.havei = True
            self.w_var3.configure(text=basename(self.iimg_fn))
        self.update_solve_labels(hint, "disabled")
        self.update_upload_labels(hint, "disabled")
        stat_bar(self, 'Idle')

    def get_filev(self):
        '''
        select the vertical image
        '''
        self.get_file("v")

    def get_fileh(self):
        '''
        select the horizontal image
        '''
        self.get_file("h")

    def get_filei(self):
        '''
        select the improvement image
        '''
        self.get_file("i")

    def solve_v(self):
        '''
        Solve the vertical image
        '''
        if self.vimg_fn == self.himg_fn:
            stat_bar(self, ('image filenames coincide - Check the Image ' +
                            'filenames'))
            return
        try:
            open(self.vimg_fn)
        except IOError:
            stat_bar(self, ("couldn't open the image - Check the Image " +
                            'filename' + self.vimg_fn))
            return
        stat_bar(self, 'Solving image...')
        img2wcs(self, self.apikey.get(), self.vimg_fn, self.vwcs_fn, "v")
        stat_bar(self, 'Idle')

    def solve_h(self):
        '''
        Solve the horizontal image
        '''
        if self.vimg_fn == self.himg_fn:
            stat_bar(self, ('image filenames coincide - Check the Image ' +
                            'filenames'))
            return
        try:
            open(self.himg_fn)
        except IOError:
            stat_bar(self, ("couldn't open the image - Check the Image " +
                            'filename' + self.himg_fn))
            return
        stat_bar(self, 'Solving image...')
        img2wcs(self, self.apikey.get(), self.himg_fn, self.hwcs_fn, "h")
        stat_bar(self, 'Idle')
        return

    def solve_i(self):
        '''
        Solve the iteration image
        '''
        try:
            open(self.iimg_fn)
        except IOError:
            stat_bar(self, ("couldn't open the image - Check the Image " +
                            'filename' + self.iimg_fn))
            return
        stat_bar(self, 'Solving image...')
        img2wcs(self, self.apikey.get(), self.iimg_fn, self.iwcs_fn, "i")
        stat_bar(self, 'Idle')
        return

    def update_display(self, ncpcrd, scale):
        import numpy
        axis = self.axis
        self.scale = scale
        self.havescale = True
        self.w_var5.configure(text=("%.2f" % scale))
        self.w_var6.configure(text=str(int(axis[0]))+','+str(int(axis[1])))
        self.w_var7.configure(text=(str(int(ncpcrd[0][0])) +
                                    ',' + str(int(ncpcrd[0][1]))))
        x1 = axis[0]
        y1 = axis[1]
        x2 = ncpcrd[0][0]
        y2 = ncpcrd[0][1]
        err = scale*numpy.sqrt((x1-x2)**2 + (y1-y2)**2)/60.0
        self.w_var8.configure(text=("%.2f" % err))
        if int(ncpcrd[0][0]) > int(axis[0]):
            inst = 'Right '
        else:
            inst = 'Left '
        inst = inst + str(int(abs(ncpcrd[0][0] - axis[0])*scale/60.0))
        self.w_var9.configure(text=inst)
        if int(ncpcrd[0][1]) > int(axis[1]):
            inst = inst + ' Down '
        else:
            inst = inst + ' Up '
        inst = inst + str(int(abs(ncpcrd[0][1] - axis[1])*scale/60.0))
        self.w_var9.configure(text=inst)

    def annotate_imp(self):
        '''
        Annotate the improvement image
        '''
        from PIL import Image
        from astropy.time import Time
        from astropy.coordinates import SkyCoord
        from astropy.coordinates import FK5
        from astropy.io import fits
        from astropy import wcs
        import numpy
        from os.path import splitext
        try:
            imi = Image.open(self.iimg_fn)
            # Load the FITS hdulist using astropy.io.fits
            hdulisti = fits.open(self.iwcs_fn)
        except IOError:
            return
        axis = self.axis
        try:
            axis[0]
        except:
            self.stat_msg = ("don't know where Axis is - Start again")
            self.w_stat.config(text=self.stat_msg)
            return
        stat_bar(self, 'Annotating...')
        t_now = Time.now()
        ncp = SkyCoord(ra=0, dec=90, frame='fk5', unit='deg', equinox=t_now)
        ncpj2000 = ncp.transform_to(FK5(equinox='J2000'))
        headi = hdulisti[0].header
        wcsi = wcs.WCS(headi)
        ncpskycrd = numpy.array([[ncpj2000.ra.deg, ncpj2000.dec.deg]],
                                numpy.float_)
        ncpcrdi = wcsi.wcs_world2pix(ncpskycrd, 1)
        polaris = numpy.array([[037.954561, 89.264109]], numpy.float_)
        poli = wcsi.wcs_world2pix(polaris, 1)
        lam = numpy.array([[259.235229, 89.037706]], numpy.float_)
        lami = wcsi.wcs_world2pix(lam, 1)
        scalei = scale(headi)
        widthi = headi['IMAGEW']
        heighti = headi['IMAGEH']
        self.update_display(ncpcrdi, scalei)
        ncpcircle(ncpcrdi, imi, scalei)
        circle(poli, imi, "White", "a")
        circle(lami, imi, "Orange", "l")
        cross([axis], imi, "Red")
        left = int(min(ncpcrdi[0][0], poli[0][0], lami[0][0], axis[0]))
        right = int(max(ncpcrdi[0][0], poli[0][0], lami[0][0], axis[0]))
        bottom = int(min(ncpcrdi[0][1], poli[0][1], lami[0][1], axis[1]))
        top = int(max(ncpcrdi[0][1], poli[0][1], lami[0][1], axis[1]))
        margin = int(2500/scalei)
        croppedi = imi.crop((max(1, left - margin), max(1, bottom - margin),
                             min(widthi, right + margin),
                             min(heighti, top + margin)))
        croppedi.load()
        crop_fn = splitext(self.iimg_fn)[0] + '_cropi.ppm'
        croppedi.save(crop_fn, 'PPM')
        self.create_imgwin(crop_fn, self.iimg_fn)
        stat_bar(self, 'Idle')

    def annotate(self):
        '''
        Find RA axis and Annotate the pair of horiz/vertical images
        '''
        from PIL import Image
        from astropy.time import Time
        import scipy.optimize
        from astropy.coordinates import SkyCoord
        from astropy.coordinates import FK5
        from astropy.io import fits
        from astropy import wcs
        import numpy
        from os.path import splitext
        #
        try:
            imh = Image.open(self.himg_fn)
            # Load the FITS hdulist using astropy.io.fits
            hdulistv = fits.open(self.vwcs_fn)
            hdulisth = fits.open(self.hwcs_fn)
        except IOError:
            return
        stat_bar(self, 'Finding RA axis...')
        t_now = Time.now()
        ncp = SkyCoord(ra=0, dec=90, frame='fk5', unit='deg', equinox=t_now)
        # NCP now, in J2000 coordinates, precess
        ncpj2000 = ncp.transform_to(FK5(equinox='J2000'))
        # Parse the WCS keywords in the primary HDU
        headv = hdulistv[0].header
        headh = hdulisth[0].header
        wcsv = wcs.WCS(headv)
        wcsh = wcs.WCS(headh)
        # sky coordinates
        ncpskycrd = numpy.array([[ncpj2000.ra.deg, ncpj2000.dec.deg]],
                                numpy.float_)
        polaris = numpy.array([[037.954561, 89.264109]], numpy.float_)
        lam = numpy.array([[259.235229, 89.037706]], numpy.float_)
        # pixel coordinates
        ncpcrdh = wcsh.wcs_world2pix(ncpskycrd, 1)
        polh = wcsh.wcs_world2pix(polaris, 1)
        lamh = wcsh.wcs_world2pix(lam, 1)
        scaleh = scale(headh)
        widthh = headh['IMAGEW']
        heighth = headh['IMAGEH']

        def displacement(coords):
            '''
            the movement of a sky object in the two images
            '''
            pixcrd1 = numpy.array([coords], numpy.float_)
            skycrd = wcsv.wcs_pix2world(pixcrd1, 1)
            pixcrd2 = wcsh.wcs_world2pix(skycrd, 1)
            return pixcrd2 - pixcrd1

        axis = scipy.optimize.broyden1(displacement, [widthh/2, heighth/2])
        self.axis = axis
        self.update_display(ncpcrdh, scaleh)
        #
        stat_bar(self, 'Annotating...')
        ncpcircle(ncpcrdh, imh, scaleh)
        circle(polh, imh, "White", "a")
        circle(lamh, imh, "Orange", "l")
        cross([axis], imh, "Red")
        left = int(min(ncpcrdh[0][0], polh[0][0], lamh[0][0], axis[0]))
        right = int(max(ncpcrdh[0][0], polh[0][0], lamh[0][0], axis[0]))
        bottom = int(min(ncpcrdh[0][1], polh[0][1], lamh[0][1], axis[1]))
        top = int(max(ncpcrdh[0][1], polh[0][1], lamh[0][1], axis[1]))
        margin = int(2500/scaleh)
        croppedh = imh.crop((max(1, left - margin), max(1, bottom - margin),
                             min(widthh, right + margin),
                             min(heighth, top + margin)))
        croppedh.load()
        crop_fn = splitext(self.himg_fn)[0] + '_croph.ppm'
        croppedh.save(crop_fn, 'PPM')
        self.create_imgwin(crop_fn, self.himg_fn)
        stat_bar(self, 'Idle')

    def create_imgwin(self, img_fn, title):
        from Tkinter import Scrollbar, Toplevel, Canvas, PhotoImage
        from os.path import basename
        # create child window
        img = PhotoImage(file=img_fn)
        win = Toplevel()
        w_wid = min(800, img.width())
        w_hei = min(800, img.height())
        win.geometry(('%dx%d' % (w_wid+28, w_hei+28)))
        win.title(basename(title))
        frame = Frame(win, bd=0)
        frame.pack()
        xscrollbar = Scrollbar(frame, orient="horizontal")
        xscrollbar.pack(side="bottom", fill="x")
        yscrollbar = Scrollbar(frame, orient="vertical")
        yscrollbar.pack(side="right", fill="y")
        canvas = Canvas(frame, bd=0, width=w_wid, height=w_hei,
                        scrollregion=(0, 0, img.width(), img.height()),
                        xscrollcommand=xscrollbar.set,
                        yscrollcommand=yscrollbar.set)
        canvas.pack(side="top", fill="both", expand=1)
        canvas.create_image(0, 0, image=img, anchor="nw")
        xscrollbar.config(command=canvas.xview)
        yscrollbar.config(command=canvas.yview)
        frame.pack()
        # next statement is important! creates reference to img
        canvas.img = img

    def update_solve_labels(self, hint, st):
        if hint == "v":
            self.w_vok.configure(state=st)
            self.w_vok.update()
        elif hint == "h":
            self.w_hok.configure(state=st)
            self.w_hok.update()
        elif hint == "i":
            self.w_iok.configure(state=st)
            self.w_iok.update()

    def update_upload_labels(self, hint, st):
        if hint == "v":
            self.w_vup.configure(state=st)
            self.w_vup.update()
        elif hint == "h":
            self.w_hup.configure(state=st)
            self.w_hup.update()
        elif hint == "i":
            self.w_iup.configure(state=st)
            self.w_iup.update()

    def create_widgets(self, master=None):
        '''
        creates the main window
        '''
        self.myparent = master
        self.myparent.title("Photo Polar Alignment")
        #
        self.menubar = Menu(master)
        self.filemenu = Menu(self.menubar, tearoff=0)
        self.helpmenu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="File", menu=self.filemenu)
        self.menubar.add_cascade(label="Help", menu=self.helpmenu)
        self.filemenu.add_command(label="Settings...",
                                  command=self.settings_open)
        self.filemenu.add_command(label="Exit", command=self.quit_method)
        self.helpmenu.add_command(label="Help", command=help_f)
        self.helpmenu.add_command(label="About...", command=about_f)
        self.myparent.config(menu=self.menubar)
        # #################################################################
        self.w_frOp = LabelFrame(master, text="Operations")
        self.w_frOp.pack(side='top', fill='x')
        #
        nxt = Button(self.w_frOp, image=self.vicon, command=self.get_filev)
        nxt.grid(row=0, column=0, sticky='ew', padx=10, pady=4, rowspan=3)
        self.w_vfn = nxt
        nxt = Button(self.w_frOp, text="Solve", command=self.solve_v)
        nxt.grid(row=0, column=1, sticky='ew', padx=10, pady=4)
        self.w_vsol = nxt
        nxt = Label(self.w_frOp, text="Uploaded", state="disabled")
        nxt.grid(row=1, column=1, sticky='ew', padx=10, pady=4)
        self.w_vup = nxt
        nxt = Label(self.w_frOp, text="Solved", state="disabled")
        nxt.grid(row=2, column=1, sticky='ew', padx=10, pady=4)
        self.w_vok = nxt
        #
        nxt = Button(self.w_frOp, image=self.hicon, command=self.get_fileh)
        nxt.grid(row=3, column=0, sticky='ew', padx=10, pady=4, rowspan=3)
        self.w_hfn = nxt
        nxt = Button(self.w_frOp, text="Solve", command=self.solve_h)
        nxt.grid(row=3, column=1, sticky='ew', padx=10, pady=4)
        self.w_hsol = nxt
        nxt = Label(self.w_frOp, text="Uploaded", state="disabled")
        nxt.grid(row=4, column=1, sticky='ew', padx=10, pady=4)
        self.w_hup = nxt
        nxt = Label(self.w_frOp, text="Solved", state="disabled")
        nxt.grid(row=5, column=1, sticky='ew', padx=10, pady=4)
        self.w_hok = nxt
        #
        nxt = Button(self.w_frOp, text="Find Polar Axis",
                     command=self.annotate)
        nxt.grid(row=6, column=0, sticky='ew', padx=10, pady=4, columnspan=2)
        self.w_ann = nxt
        #
        nxt = Button(self.w_frOp, image=self.iicon, command=self.get_filei)
        nxt.grid(row=0, column=3, sticky='ew', padx=10, pady=4, rowspan=3)
        self.w_imp = nxt
        nxt = Button(self.w_frOp, text="Solve", command=self.solve_i)
        nxt.grid(row=0, column=4, sticky='ew', padx=10, pady=4)
        self.w_isol = nxt
        nxt = Label(self.w_frOp, text="Uploaded", state="disabled")
        nxt.grid(row=1, column=4, sticky='ew', padx=10, pady=4)
        self.w_iup = nxt
        nxt = Label(self.w_frOp, text="Solved", state="disabled")
        nxt.grid(row=2, column=4, sticky='ew', padx=10, pady=4)
        self.w_iok = nxt
        #
        nxt = Button(self.w_frOp, text="Show Improvement",
                     command=self.annotate_imp)
        nxt.grid(row=6, column=3, sticky='ew', padx=10, pady=4, columnspan=2)
        self.w_anni = nxt
        # #################################################################
        nxt = Frame(master)
        self.w_fr2 = nxt
        nxt = LabelFrame(self.w_fr2, borderwidth=2, relief='ridge',
                         text="Given")
        nxt.grid(row=0, column=0)
        self.w_frVar = nxt
        nxt = Label(self.w_frVar, anchor="w", text="Vertical:")
        nxt.grid(row=0, column=0, sticky="w")
        nxt = Label(self.w_frVar, text="---------")
        nxt.grid(row=0, column=1, sticky="e")
        self.w_var1 = nxt
        nxt = Label(self.w_frVar, text="Horizontal:")
        nxt.grid(row=1, column=0, sticky="w")
        nxt = Label(self.w_frVar, text="---------")
        nxt.grid(row=1, column=1, sticky="e")
        self.w_var2 = nxt
        nxt = Label(self.w_frVar, text="Improved:")
        nxt.grid(row=2, column=0, sticky="w")
        nxt = Label(self.w_frVar, text="---------")
        nxt.grid(row=2, column=1, sticky="e")
        self.w_var3 = nxt
        nxt = Label(self.w_frVar, text="API key:")
        nxt.grid(row=3, column=0, sticky="w")
        nxt = Label(self.w_frVar, text=self.apikey.get())
        nxt.grid(row=3, column=1, sticky="e")
        self.w_var4 = nxt
        # #####################
        nxt = LabelFrame(self.w_fr2, borderwidth=2, relief='ridge',
                         text="Computed")
        nxt.grid(row=0, column=1)
        self.w_frComp = nxt
        nxt = Label(self.w_frComp, text="Scale (arcsec/pixel):")
        nxt.grid(row=0, column=0, sticky="w")
        if self.havescale:
            nxt = Label(self.w_frComp, text=self.scale)
        else:
            nxt = Label(self.w_frComp, text="--.--")
        nxt.grid(row=0, column=1, sticky="e")
        self.w_var5 = nxt
        nxt = Label(self.w_frComp, text="RA axis position:")
        nxt.grid(row=1, column=0, sticky="w")
        nxt = Label(self.w_frComp, text="---,---")
        nxt.grid(row=1, column=1, sticky="e")
        self.w_var6 = nxt
        nxt = Label(self.w_frComp, text="NCP position:")
        nxt.grid(row=2, column=0, sticky="w")
        nxt = Label(self.w_frComp, text="---,---")
        nxt.grid(row=2, column=1, sticky="e")
        self.w_var7 = nxt
        nxt = Label(self.w_frComp, text="Error (arcmin):")
        nxt.grid(row=3, column=0, sticky="w")
        nxt = Label(self.w_frComp, text="--.--")
        nxt.grid(row=3, column=1, sticky="e")
        self.w_var8 = nxt
        self.w_fr2.pack(side="top", fill="x")
        # #################################################################
        nxt = LabelFrame(master, borderwidth=2, relief='ridge',
                         text="Move (arcmin)")
        nxt.pack(side='top', fill='x')
        self.w_frMo = nxt
        nxt = Label(self.w_frMo, anchor='center', font='-weight bold -size 14')
        nxt.pack(anchor='center')
        self.w_var9 = nxt
        # #################################################################
        nxt = LabelFrame(master, borderwidth=2, relief='ridge', text="Status")
        nxt.pack(side='bottom', fill='x')
        self.w_frSt = nxt
        nxt = Label(self.w_frSt, anchor='w', text=self.stat_msg)
        nxt.pack(anchor='w')
        self.w_stat = nxt

    def __init__(self, master=None):
        import ConfigParser
        # the pixel coords of the RA axis, if solution exists
        self.axis = None
        self.havea = False
        # the Settings window
        self.settings_win = None
        # the User preferences file
        self.cfgfn = 'PPA.ini'
        # Read the User preferences
        self.config = ConfigParser.ConfigParser()
        self.config.read(self.cfgfn)
        # ...the key
        k_ini = None
        if self.config.has_section('nova'):
            k_ini = self.config.get('nova', 'apikey', None)
        self.apikey = StringVar(value=k_ini)
        # ...the Image directory
        self.imgdir = None
        if self.config.has_section('file'):
            self.imgdir = self.config.get('file', 'imgdir', None)
        # ...geometry
        self.usergeo = None
        if self.config.has_section('appearance'):
            self.usergeo = self.config.get('appearance', 'geometry', None)
            master.geometry(self.usergeo)
        # the filenames of images
        self.vimg_fn = ""
        self.havev = False
        self.himg_fn = ""
        self.haveh = False
        self.iimg_fn = ""
        self.havei = False
        # the filenames of the .wcs solutions
        self.vwcs_fn = ""
        self.hwcs_fn = ""
        self.iwcs_fn = ""
        # the button icons
        self.vicon = PhotoImage(file='v2_2.ppm')
        self.hicon = PhotoImage(file='h2_2.ppm')
        self.iicon = PhotoImage(file='i2_2.ppm')
        # the solved image scale
        self.havescale = False
        self.filemenu = None
        self.helpmenu = None
        self.menubar = None
        self.w_frOp = None
        self.w_frVar = None
        self.w_frSt = None
        self.w_stat = None
        self.w_vfn = None
        self.w_hfn = None
        self.w_cal = None
        self.myparent = None
        self.stat_msg = 'Idle'
        Frame.__init__(self, master)
        self.create_widgets(master)
        self.pack()
        #

ROOT = Tk()
ROOT.geometry("440x440+300+300")
APP = PhotoPolarAlign(master=ROOT)
ROOT.mainloop()
